package ace.cmp.security.core.impl.service.impl;

import ace.cmp.security.core.impl.service.SpringSecurityService;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;

/**
 * @author caspar
 * @date 2023/8/28 15:48 {@link SecurityExpressionOperations}
 */
@Slf4j
public abstract class AbstractSpringSecurityService<TCredentials>
    implements SpringSecurityService<TCredentials>, SecurityExpressionOperations {

  public AbstractSpringSecurityService() {
  }

  public AbstractSpringSecurityService(String defaultRolePrefix, String administratorRoleKeySuffix) {
    this.defaultRolePrefix = defaultRolePrefix;
    this.administratorRoleKeySuffix = administratorRoleKeySuffix;
  }

  private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

  private RoleHierarchy roleHierarchy;

  @Getter
  private String defaultRolePrefix = "ROLE_";

  /**
   * 超级管理员角色key
   */
  @Getter
  @Setter
  private String administratorRoleKeySuffix = "_ADMINISTRATOR__#SYSTEM#";

  private PermissionEvaluator permissionEvaluator;


  @Override
  public boolean hasAuthority(String authority) {
    return hasAnyAuthority(authority);
  }

  @Override
  public boolean hasAnyAuthority(String... authorities) {
    return hasAnyAuthorityName(null, authorities);
  }

  @Override
  public boolean hasRole(String role) {
    return hasAnyRole(role);
  }

  @Override
  public boolean hasAnyRole(String... roles) {
    return hasAnyAuthorityName(this.defaultRolePrefix, roles);
  }

  private boolean hasAnyAuthorityName(String prefix, String... roles) {
    Set<String> roleSet = getAuthoritySet();
    String administratorRoleKeyWithDefaultRolePrefix = this.getAdministratorRoleKeyWithDefaultRolePrefix();
    for (String role : roles) {
      String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
      if (defaultedRole.equals(administratorRoleKeyWithDefaultRolePrefix)) {
        return true;
      }
      if (roleSet.contains(defaultedRole)) {
        return true;
      }
    }
    return false;
  }

  @Override
  public boolean permitAll() {
    return true;
  }

  @Override
  public boolean denyAll() {
    return false;
  }

  @Override
  public boolean isAnonymous() {
    return this.trustResolver.isAnonymous(getAuthentication());
  }

  @Override
  public boolean isSuperAdministrator() {
    return this.hasRole(this.administratorRoleKeySuffix);
  }

  @Override
  public boolean isAuthenticated() {
    return !isAnonymous();
  }

  @Override
  public boolean isRememberMe() {
    return this.trustResolver.isRememberMe(getAuthentication());
  }

  @Override
  public boolean isFullyAuthenticated() {
    Authentication authentication = getAuthentication();
    return this.trustResolver.isFullyAuthenticated(authentication);
  }

  /**
   * Convenience method to access {@link Authentication#getPrincipal()} from
   * {@link #getAuthentication()}
   *
   * @return
   */
  public Object getPrincipal() {
    return getAuthentication().getPrincipal();
  }

  public void setTrustResolver(AuthenticationTrustResolver trustResolver) {
    this.trustResolver = trustResolver;
  }

  public void setRoleHierarchy(RoleHierarchy roleHierarchy) {
    this.roleHierarchy = roleHierarchy;
  }

  /**
   * <p>
   * Sets the default prefix to be added to {@link #hasAnyRole(String...)} or
   * {@link #hasRole(String)}. For example, if hasRole("ADMIN") or hasRole("ROLE_ADMIN")
   * is passed in, then the role ROLE_ADMIN will be used when the defaultRolePrefix is
   * "ROLE_" (default).
   * </p>
   *
   * <p>
   * If null or empty, then no default role prefix is used.
   * </p>
   *
   * @param defaultRolePrefix the default prefix to add to roles. Default "ROLE_".
   */
  public void setDefaultRolePrefix(String defaultRolePrefix) {
    this.defaultRolePrefix = defaultRolePrefix;
  }

  private Set<String> getAuthoritySet() {
    Authentication authentication = getAuthentication();
    if (authentication == null) {
      return Collections.EMPTY_SET;
    }
    Collection<? extends GrantedAuthority> userAuthorities = getAuthentication().getAuthorities();
    if (this.roleHierarchy != null) {
      userAuthorities = this.roleHierarchy.getReachableGrantedAuthorities(userAuthorities);
    }
    return AuthorityUtils.authorityListToSet(userAuthorities);
  }

  @Override
  public boolean hasPermission(Object target, Object permission) {
    return this.permissionEvaluator.hasPermission(getAuthentication(), target, permission);
  }

  @Override
  public boolean hasPermission(Object targetId, String targetType, Object permission) {
    return this.permissionEvaluator.hasPermission(
        getAuthentication(), (Serializable) targetId, targetType, permission);
  }

  public void setPermissionEvaluator(PermissionEvaluator permissionEvaluator) {
    this.permissionEvaluator = permissionEvaluator;
  }

  /**
   * Prefixes role with defaultRolePrefix if defaultRolePrefix is non-null and if role
   * does not already start with defaultRolePrefix.
   *
   * @param defaultRolePrefix
   * @param role
   * @return
   */
  private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
    if (role == null) {
      return role;
    }
    if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
      return role;
    }
    if (role.startsWith(defaultRolePrefix)) {
      return role;
    }
    return defaultRolePrefix + role;
  }

  /**
   * 获取超级管理员角色key.
   *
   * @return 超级管理员角色key
   */
  private String getAdministratorRoleKeyWithDefaultRolePrefix() {
    return this.defaultRolePrefix + this.administratorRoleKeySuffix;
  }
}
